﻿using System;
using System.IO;

/*
    DbaseReader
    Nicholas Bambury 
    Created August 2011.

    This is a very basic C# DLL which provides simple functions to read data from a DBase 3, 4 or 5 database.

    Licence - Microsoft  Public Licence. See http://dbasereader.codeplex.com/license for details.
 
*/

namespace DBaseReader
{
    /// <summary>
    /// DBaseReader
    /// </summary>
    public class DBaseReader
    {
        private string path = null;
        private string fileName = null;

        private int numberOfRecords = 0;
        private int numberOfValidRecords = 0;
        private int lengthOfHeaderStructure = 0;
        private int lengthOfRecord = 0;
        private int dataOffset = 0;
        private int numberOfFields = 0;
        private bool isEncrypted = false;

        // The byte[] buffers are all used to read in data from the file and then cleared once
        // no longer used to free up memory.
        private byte[] fileBuffer = null;
        private byte[] fieldDescriptorBuffer = null;

        private System.Collections.Generic.SortedList<int, FieldInfo> fieldSortedList = null;   // Stores Field Info    (Contains FieldInfo Struct)
        private System.Collections.ArrayList recordsArray = null;                               // Array of all records (Contains Generic.List<string>)
        private System.Collections.Generic.SortedList<int, int> maxFieldSize = null;            // Max field size based on all data

// ************************************************************************************************
// DBase DBF file structure offSets (DBase 3, 4 and 5)
// ************************************************************************************************

        // Header Offsets
        static private int versionNumberOffset = 0;
        static private int yearLastUpdatedOffset = 1;           // Add value to 1900 base year. (1900-2155) 
        static private int encryptionFlagOffset = 15;

        static private int numberOfRecordsOffset = 4;
        static private int lengthOfHeaderStructureOffSet = 8;
        static private int lengthOfRecordOffset = 10;
        static private int fieldDescriptorArrayOffSet = 32;

        // Field Descriptor Offsets
        static private int fieldDescriptorElementLength = 32;

        static private int fieldDesc_FieldNameEndOffSet = 10;
        static private int fieldDesc_FieldTypeOffSet = 11;
        static private int fieldDesc_FieldLengthOffSet = 16;

// ************************************************************************************************
// DBase DBF values
// ************************************************************************************************

        static private int recordDeleted = 42;  // 2Ah
        static private int recordValid = 32;    // 20h

        static private int dataNotEncrypted = 0;    // 00h
        static private int dataEncrypted = 1;       // 01h

// ************************************************************************************************
// DBase Data Types
// ************************************************************************************************

        private enum DateTimeType
        {
            DbaseDate = 1,
            DbaseDateTime = 2,
            DBaseTimeStamp = 3
        };
        
        // DBase Field Structure
        private struct FieldInfo
        {
            public int fieldNumber;
            public int fieldLength;
            public char fieldTypeCode;
            public string fieldName;            

            public string fieldType()
            {
                string temp;
                return GetDataType(fieldTypeCode, out temp);
            }
        }

// ************************************************************************************************
// Exception Logging
// ************************************************************************************************

        private StreamWriter swErrorLog;

// ************************************************************************************************
// Static methods
// ************************************************************************************************

        static private string GetDataType(char fieldType, out string dotNetType)
        {
            string dBaseType = null;
            dotNetType = null;

            switch (fieldType)
            {
                case 'C':
                    dBaseType = "Character";
                    dotNetType = "string";
                    break;
                case 'N':
                    dBaseType = "Number";
                    dotNetType = "float";               // Needs to be a float to cover all possible numeric values allowed
                    break;
                case 'L':
                    dBaseType = "Logical";
                    dotNetType = "bool";
                    break;
                case 'D':
                    dBaseType = "Date";
                    dotNetType = "DateTime";
                    break;
                case 'M':
                    dBaseType = "Memo";
                    dotNetType = "string";
                    break;
                case 'F':
                    dBaseType = "Floating Point";
                    dotNetType = "float";
                    break;
                case 'B':
                    dBaseType = "Binary";
                    dotNetType = "NOT SUPPORTED";        // Not sure what to do with these
                    break;
                case 'G':
                    dBaseType = "General";
                    dotNetType = "NOT SUPPORTED";        // Not sure what to do with these
                    break;
                case 'P':
                    dBaseType = "Picture";
                    dotNetType = "NOT SUPPORTED";        // Not sure what to do with these
                    break;
                case 'Y':
                    dBaseType = "Currency";
                    dotNetType = "double";
                    break;
                case 'T':
                    dBaseType = "DateTime";
                    dotNetType = "DateTime";
                    break;
                case 'I':
                    dBaseType = "Integer";
                    dotNetType = "int";
                    break;
                case 'V':
                    dBaseType = "VaniField";
                    dotNetType = "NOT SUPPORTED";        // Not sure what to do with these
                    break;
                case 'X':
                    dBaseType = "Variant";
                    dotNetType = "NOT SUPPORTED";        // Not sure what to do with these
                    break;
                case '@':
                    dBaseType = "TimeStamp";
                    dotNetType = "DateTime";
                    break;
                case 'O':
                    dBaseType = "Double";
                    dotNetType = "double";
                    break;
                case '+':
                    dBaseType = "AutoIncrement (Long)";
                    dotNetType = "long";
                    break;

                default:
                    dBaseType = "Unknown";
                    dotNetType = "Unknown";
                    break;
            }

            return dBaseType;
        }

// ************************************************************************************************
// Public Methods
// ************************************************************************************************

        /// <summary>
        /// OpenDBaseDBF
        /// </summary>
        /// <param name="dbfFile">DBase DBF file name and path</param>
        /// <returns>True or False depending on success</returns>
        public bool OpenDBaseDBF(string dbfFile)
        {
            try
            {
                SplitFileNameAndPath(dbfFile);

                CreateErrorLogFile();

                fileName = dbfFile;
                if (ReadDbfFile())
                {
                    bool allOK = true;

                    if (!GetDbfFileInfo())
                        allOK = false;

                    if (!GetFileDescriptorData())
                        allOK = false;

                    if (!SeperateOutFieldDescriptors())
                        allOK = false;

                    ClearFieldDescriptorBuffer();       // We need the memory! and no longer need the fieldDescriptorBuffer

                    if (!SeperateOutRecords(false))
                        allOK = false;

                    ClearFileBuffer();                  // We need the memory! and no longer need the fileBuffer

                    if (allOK)
                        return true;
                    else
                        return false;
                }
                else
                    return false;
            }
            catch (Exception ex)
            {
                LogError("OpenDBaseDBF", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// ExportType Enum
        /// txtFile, csvFile
        /// </summary>
        public enum ExportType
        {
            /// txtFile
            txtFile = 1,
            /// csvFile
            csvFile = 2
        };

        /// <summary>
        /// ExportData
        /// 
        /// Exports data to cvsFile
        /// </summary>
        /// <param name="exportType">enum Export Type - txtFile, csvFile</param>
        /// <returns>True or false depending on success</returns>
        public bool ExportData(ExportType exportType)
        {
            try
            {
                switch (exportType)
                {
                    case ExportType.txtFile:
                        {
                            break;
                        }
                    case ExportType.csvFile:
                        {
                            System.IO.File.WriteAllLines(path + "\\csvFile.csv", (String[])GetRecords().ToArray());
                            break;
                        }
                    default:
                        break;
                }

                return true;
            }
            catch (ArgumentException aEx)
            {
                LogError("ExportDataToText - Argument Exception", aEx.Message);
                return false;
            }
            catch (System.IO.FileNotFoundException fnfEx)
            {
                LogError("ExportDataToText - File Not Found Exception", fnfEx.Message);
                return false;
            }
            catch (System.IO.DirectoryNotFoundException dnfEx)
            {
                LogError("ExportDataToText - Directory Not Found Exception", dnfEx.Message);
                return false;
            }
            catch (System.IO.IOException ioEx)
            {
                LogError("ExportDataToText - IO Exception", ioEx.Message);
                return false;
            }
            catch (Exception ex)
            {
                LogError("ExportDataToText", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// NumberOfRecords
        /// </summary>
        /// <returns>int - number of records in the database</returns>
        public int NumberOfRecords()
        {
            return numberOfRecords;
        }

        /// <summary>
        /// NumberOfValidRecords
        /// </summary>
        /// <returns>int - nmuber of valid records in the database</returns>
        public int NumberOfValidRecords()
        {
            return numberOfValidRecords;
        }

        /// <summary>
        /// NumberOfFields
        /// </summary>
        /// <returns>int - number of fields in each record</returns>
        public int NumberOfFields()
        {
            return numberOfFields;
        }

        /// <summary>
        /// GetFieldNames
        /// </summary>
        /// <returns>Comma Delimited string of the field names</returns>
        public string GetFieldNames()
        {
            return GetCommaDelimitedFieldNames();
        }

        /// <summary>
        /// GetIsEncrypted
        /// </summary>
        /// <returns>bool - to represent if the DBF file is encrypted or not</returns>
        public bool GetIsEncrypted()
        {
            return isEncrypted;
        }

        /// <summary>
        /// GetFieldsInfo
        /// </summary>
        /// <returns>System.Collections.ArrayList of comma delimited field info: Field No, Field Name, Field Type, Field Length</returns>
        public System.Collections.Generic.List<string> GetFieldsInfo()
        {
            System.Collections.Generic.List<string> fieldInfoList = new System.Collections.Generic.List<string>();

            for (int i = 1; i <= numberOfFields; i++)
            {
                fieldInfoList.Add(GetCommaDelimitedFieldInfo(i));
            }

            return fieldInfoList;
        }

        /// <summary>
        /// GetFieldsInfoExtended
        /// </summary>
        /// <returns>System.Collections.ArrayList of comma delimited field info: Field No, Field Name, Field Type, Field Length, Max Size Used</returns>
        public System.Collections.Generic.List<string> GetFieldsInfoExtended()
        {
            System.Collections.Generic.List<string> fieldInfoList = new System.Collections.Generic.List<string>();

            for (int i = 1; i <= numberOfFields; i++)
            {
                string temp = GetCommaDelimitedFieldInfo(i);
                temp = temp + "," + maxFieldSize[i];
                fieldInfoList.Add(temp);
            }

            return fieldInfoList;
        }

        /// <summary>
        /// GetRecords
        /// </summary>
        /// <returns>System.Collections.ArrayList of comma delimited record data. To be used to grab data in simplest form.</returns>
        public System.Collections.Generic.List<string> GetRecords()
        {
            try
            {
                System.Collections.Generic.List<string> recordList = new System.Collections.Generic.List<string>();

                recordList.Add(GetFieldNames());                     // Include Field names.

                for (int i = 0; i < numberOfRecords; i++)
                {
                    recordList.Add(GetCommaDelimitedRecord(i));
                }

                return recordList;
            }
            catch (Exception ex)
            {
                LogError("GetRecords", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// Public GetRecords
        /// 
        /// Gets all data for a given list of fields and returns the data in a ArrayList containing comma delimited string for each record.
        /// </summary>
        /// <param name="fieldList">System.Collections.ArrayList of fields (no.) to include for data extract.</param>
        /// <returns>System.Collections.ArrayList of comma delimited record data. </returns>
        public System.Collections.Generic.List<string> GetRecords(System.Collections.Generic.List<int> fieldList)
        {
            try
            {
                System.Collections.Generic.List<string> recordList = new System.Collections.Generic.List<string>();

                string[] fieldNames = GetFieldNames().Split(',');

                string fieldNamesCommaDelimited = "";

                foreach (int fieldNo in fieldList)
                {
                    if (String.IsNullOrEmpty(fieldNamesCommaDelimited))
                        fieldNamesCommaDelimited = fieldNames[fieldNo-1];
                    else
                        fieldNamesCommaDelimited = fieldNamesCommaDelimited + "," + fieldNames[fieldNo-1];                   
                }

                recordList.Add("Record No. ," + fieldNamesCommaDelimited);

                for (int i = 0; i < numberOfRecords; i++)
                {
                    System.Collections.Generic.List<string> record = (System.Collections.Generic.List<string>)recordsArray[i];
                    
                    string recordData = "";

                    foreach (int fieldNo in fieldList)
                    {
                        if (String.IsNullOrEmpty(recordData))
                            recordData = "\"" +i+1 +"\"" + "," +"\"" + (string)record[fieldNo-1] + "\"";
                        else
                            recordData = recordData + "," + "\"" + (string)record[fieldNo-1] + "\"";
                    }

                    recordList.Add(recordData);
                }                

                return recordList;
            }
            catch (Exception ex)
            {
                LogError("GetRecords fieldList", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// GetUnusedFields
        /// </summary>
        /// <returns>System.Collections.ArrayList of the fields without any data in</returns>
        public System.Collections.Generic.List<string> GetUnusedFields()
        {
            try
            {
                System.Collections.Generic.List<string> fieldList = new System.Collections.Generic.List<string>();

                System.Collections.SortedList recordFieldCount = AnalyseRecords_FieldCount();

                if (recordFieldCount.Count > 0)
                {
                    for (int i = 0; i < numberOfFields; i++)
                    {
                        int count = (int)recordFieldCount[i];

                        if (count == 0)
                        {
                            fieldList.Add(GetFieldName(i + 1));
                        }
                    }
                }

                return fieldList;
            }
            catch (Exception ex)
            {
                LogError("GetUnsedFields", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// GetUsedFields
        /// </summary>
        /// <returns>System.Collections.ArrayList of the fields which contain data</returns>
        public System.Collections.Generic.List<string> GetUsedFields()
        {
            try
            {
                System.Collections.Generic.List<string> fieldList = new System.Collections.Generic.List<string>();

                System.Collections.SortedList recordFieldCount = AnalyseRecords_FieldCount();

                if (recordFieldCount.Count > 0)
                {
                    for (int i = 0; i < numberOfFields; i++)
                    {
                        int count = (int)recordFieldCount[i];

                        if (count > 0)
                        {
                            fieldList.Add(GetFieldName(i + 1));
                        }
                    }
                }

                return fieldList;
            }
            catch (Exception ex)
            {
                LogError("GetUsedFields", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// GetUsedFieldData
        /// 
        /// This method returns all records, each record contains all the fields which have been found to be
        /// used fields by the GetUsedFields method. So not all records will contain data within all fields,
        /// but no field will be included in any records if the field is never used throughout the database.
        /// </summary>
        /// <returns>System.Collections.ArrayList of records with only used data fields.</returns>
        public System.Collections.Generic.List<string> GetUsedFieldData()
        {
            try
            {
                System.Collections.Generic.List<string> usedFields = GetUsedFields();
                System.Collections.Generic.List<int> usedFiledsno = new System.Collections.Generic.List<int>();

                foreach (string field in usedFields)
                {
                    foreach(System.Collections.Generic.KeyValuePair<int, FieldInfo> kvp in fieldSortedList)
                    {                        
                        FieldInfo fieldInfo = (FieldInfo)kvp.Value;

                        if (field == fieldInfo.fieldName)
                            usedFiledsno.Add(fieldInfo.fieldNumber);
                    }
                }

                return GetRecords(usedFiledsno);
            }
            catch (Exception ex)
            {
                LogError("GetUsedFieldData", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// ExportUsedFieldsData
        /// 
        /// Exports Record data, removing any fields not used by any records in the database.
        /// </summary>
        /// <returns>True or false, depending on success</returns>
        public bool ExportUsedFieldsData()
        {
            try
            {
                System.IO.File.WriteAllLines(path + "\\UsedFields.csv", (string[])GetUsedFieldData().ToArray());
                return true;
            }
            catch (ArgumentException aEx)
            {
                LogError("ExportUsedFields - Argument Exception", aEx.Message);
                return false;
            }
            catch (System.IO.FileNotFoundException fnfEx)
            {
                LogError("ExportUsedFields - File Not Found Exception", fnfEx.Message);
                return false;
            }
            catch (System.IO.DirectoryNotFoundException dnfEx)
            {
                LogError("ExportUsedFields - Directory Not Found Exception", dnfEx.Message);
                return false;
            }
            catch (System.IO.IOException ioEx)
            {
                LogError("ExportUsedFields - IO Exception", ioEx.Message);
                return false;
            }
            catch (Exception ex)
            {
                LogError("ExportUsedFields", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// ExportUnsedFields
        /// 
        /// Exports a list of Unsed Fields.
        /// </summary>
        /// <returns>True or false depending on success</returns>
        public bool ExportUnusedFields()
        {
            try
            {
                string temp = "";

                System.Collections.Generic.List<string> tempDataArray = GetUnusedFields();

                System.Collections.Generic.List<string> unsedFieldsList = new System.Collections.Generic.List<string>();

                for (int i = 0; i < tempDataArray.Count; i++)
                {
                    temp = (string)tempDataArray[i];

                    foreach(System.Collections.Generic.KeyValuePair<int, FieldInfo> kvp in fieldSortedList)
                    {
                        FieldInfo fieldInfo = (FieldInfo)kvp.Value;

                        if (fieldInfo.fieldName == (string)tempDataArray[i])
                        {
                            temp = temp + "," + kvp.Key.ToString() + "," +fieldInfo.fieldType() + "," + fieldInfo.fieldLength;
                            break;
                        }
                    }

                    unsedFieldsList.Add(temp);
                }

                System.IO.File.WriteAllLines(path + "\\UnusedFields.csv", (string[])unsedFieldsList.ToArray());
                return true;
            }
            catch (ArgumentException aEx)
            {
                LogError("ExportUnusedFields - Argument Exception", aEx.Message);
                return false;
            }
            catch (System.IO.FileNotFoundException fnfEx)
            {
                LogError("ExportUnusedFields - File Not Found Exception", fnfEx.Message);
                return false;
            }
            catch (System.IO.DirectoryNotFoundException dnfEx)
            {
                LogError("ExportUnusedFields - Directory Not Found Exception", dnfEx.Message);
                return false;
            }
            catch (System.IO.IOException ioEx)
            {
                LogError("ExportUnusedFields - IO Exception", ioEx.Message);
                return false;
            }
            catch (Exception ex)
            {
                LogError("ExportUnusedFields", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// ExportFieldData
        /// 
        /// Exports a field data.
        /// </summary>
        /// <returns>True or false depending on success</returns>
        public bool ExportFieldData()
        {
            try
            {
                System.IO.File.WriteAllLines(path + "\\FieldData.csv", (String[])GetFieldsInfo().ToArray());
                System.IO.File.WriteAllLines(path + "\\FieldDataExt.csv", (String[])GetFieldsInfoExtended().ToArray());
                return true;
            }
            catch (ArgumentException aEx)
            {
                LogError("ExportFieldData - Argument Exception", aEx.Message);
                return false;
            }
            catch (System.IO.FileNotFoundException fnfEx)
            {
                LogError("ExportFieldData - File Not Found Exception", fnfEx.Message);
                return false;
            }
            catch (System.IO.DirectoryNotFoundException dnfEx)
            {
                LogError("ExportFieldData - Directory Not Found Exception", dnfEx.Message);
                return false;
            }
            catch (System.IO.IOException ioEx)
            {
                LogError("ExportFieldData - IO Exception", ioEx.Message);
                return false;
            }
            catch (Exception ex)
            {
                LogError("ExportFieldData", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// GetDataAsCorrectType
        /// 
        /// Gets the data from a record, figures out what dataType it is and returns an object containing the
        /// data of that type. Data has to be parsed from the text form it is stored in.
        /// </summary>
        /// <param name="field">Field no</param>
        /// <param name="record">Record no</param>
        /// <returns>object - containing what ever data in whatever dataType based on the field input</returns>
        public object GetDataAsCorrectType(int field, int record)
        {
            System.Collections.Generic.List<string> recordData = (System.Collections.Generic.List<string>)recordsArray[record];

            string fieldData = recordData[field-1];

            FieldInfo fieldInfo = fieldSortedList[field];

            Object obj = null;

            string typeTemp = "";
            GetDataType(fieldInfo.fieldTypeCode, out typeTemp);

            if (typeTemp == "string")
            {
                // No converstion required as all fields are stored as text.
                obj = fieldData;
            }
            if (typeTemp == "int")
            {
                obj = StringToInt(fieldData);
            }
            if (typeTemp == "long")
            {
                obj = StringToLong(fieldData);
            }
            if (typeTemp == "double")
            {                
                obj = StringToDouble(fieldData);
            }
            if (typeTemp == "float")
            {
                obj = StringToFloat(fieldData);
            }
            if (typeTemp == "bool")
            {                
                obj = StringToBool(fieldData);
            }
            if (typeTemp == "DateTime")
            {                
                obj = StringToDateTime(fieldData, DateTimeType.DbaseDate);
            }

            return obj;
        }

// ************************************************************************************************
// Private methods
// ************************************************************************************************

        /// <summary>
        /// Private ReadDbfFile
        /// 
        /// Reads DBase file into a private byte array variable.
        /// </summary>
        /// <returns>True or False depending on success</returns>
        private bool ReadDbfFile()
        {
            try
            {
                FileStream fs = new System.IO.FileStream(fileName, System.IO.FileMode.Open);

                fileBuffer = new byte[fs.Length];

                fs.Read(fileBuffer, 0, (int)fs.Length);
                fs.Close();

                return true;
            }
            catch (System.ArgumentException aEx)
            {
                fileBuffer = null;

                LogError("ReadDbfFile - Argument Exception", aEx.Message);
                return false;
            }
            catch (System.IO.FileNotFoundException fnfEx)
            {
                fileBuffer = null;

                LogError("ReadDbfFile - File Not Found Exception", fnfEx.Message);
                return false;
            }
            catch (System.IO.DirectoryNotFoundException dnfEx)
            {
                fileBuffer = null;

                LogError("ReadDbfFile - Direcotry Not Found Exception", dnfEx.Message);
                return false;
            }
            catch (System.IO.IOException ioEx)
            {
                fileBuffer = null;

                LogError("ReadDbfFile - IO Exception", ioEx.Message);
                return false;
            }           
            catch (Exception ex)
            {
                fileBuffer = null;

                LogError("ReadDbfFile - General Exception", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// Private GetDbfFileInfo
        /// 
        /// Retrieves key information about the Dbf file.
        /// Number of Records
        /// Length of Records
        /// Header Structure info
        /// If Data is Encrypted or not (also logged to Error Log)
        /// </summary>
        /// <returns>True or False depending on success</returns>
        private bool GetDbfFileInfo()
        {
            try
            {
                numberOfRecords = ReadInt16(numberOfRecordsOffset, fileBuffer);
                lengthOfHeaderStructure = ReadInt16(lengthOfHeaderStructureOffSet, fileBuffer);
                lengthOfRecord = ReadInt16(lengthOfRecordOffset, fileBuffer);

                ReadInt16(versionNumberOffset, fileBuffer);
                ReadInt16(yearLastUpdatedOffset, fileBuffer);

                if (ReadInt16(encryptionFlagOffset, fileBuffer) == dataEncrypted)
                {
                    isEncrypted = true;
                    LogError("GetDbfFileInfo", "DBF is encrypted");
                }
                else if (ReadInt16(encryptionFlagOffset, fileBuffer) == dataNotEncrypted)
                    isEncrypted = false;

                dataOffset = lengthOfHeaderStructure;
                               
                return true;
            }
            catch (Exception ex)
            {
                numberOfRecords = 0;
                lengthOfHeaderStructure = 0;
                lengthOfRecord = 0;

                LogError("GetDbfFileInfo", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// Private GetFileDescriptorData
        /// 
        /// Retrieves the File DescriptorData
        /// </summary>
        /// <returns>True or false depending on success</returns>
        private bool GetFileDescriptorData()
        {
            try
            {
                numberOfFields = (lengthOfHeaderStructure - (fieldDescriptorArrayOffSet + 1)) / fieldDescriptorElementLength;

                fieldDescriptorBuffer = new byte[lengthOfHeaderStructure - fieldDescriptorArrayOffSet + 1];

                for (int i = fieldDescriptorArrayOffSet; i <= lengthOfHeaderStructure; i++)
                {
                    fieldDescriptorBuffer[i - fieldDescriptorArrayOffSet] = fileBuffer[i];
                }

                return true;
            }
            catch (Exception ex)
            {
                numberOfFields = 0;
                fieldDescriptorBuffer = null;

                LogError("GetFileDescriptorData", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// Private SeperateOutFieldDescriptors
        /// 
        /// Seperates out field infomation and stores in a private SortedList.
        /// Checks the number of field data items retreived and sorted in SortedList matches the number of fields expect.
        /// 
        /// Field Data Sorted is, Field No., Name, DataType and Length.
        /// </summary>
        /// <returns>True or false depending on success</returns>
        private bool SeperateOutFieldDescriptors()
        {
            try
            {
                fieldSortedList = new System.Collections.Generic.SortedList<int, FieldInfo>();

                System.Collections.IEnumerator iEnum = fieldDescriptorBuffer.GetEnumerator();

                string fieldName = null;
                char fieldType = ' ';
                int fieldLength = 0;

                int pos = 0;
                int fieldNumber = 1;

                while (iEnum.MoveNext())
                {
                    if (pos < fieldDescriptorElementLength - 1)
                    {
                        if (pos <= fieldDesc_FieldNameEndOffSet)        // Field Name
                        {
                            if (!Char.IsControl((char)(byte)iEnum.Current))     // We don't want any control chars included in the name.
                                fieldName = fieldName + (char)(byte)iEnum.Current;
                        }
                        else if (pos == fieldDesc_FieldTypeOffSet)      // Field Type
                            fieldType = (char)(byte)iEnum.Current;

                        else if (pos == fieldDesc_FieldLengthOffSet)    // Field Length
                            fieldLength = (int)(byte)iEnum.Current;

                        pos++;
                    }
                    else
                    {
                        FieldInfo fieldInfo = new FieldInfo();

                        fieldInfo.fieldNumber = fieldNumber;
                        fieldInfo.fieldName = fieldName;
                        fieldInfo.fieldTypeCode = fieldType;
                        fieldInfo.fieldLength = fieldLength;

                        fieldSortedList.Add(fieldNumber, fieldInfo);
                        fieldNumber++;

                        pos = 0;
                        fieldName = "";
                        fieldType = ' ';
                        fieldLength = 0;
                    }
                }

                if (fieldSortedList.Count != numberOfFields)
                    LogError("SeperateOutFieldDescriptors", "Error - Field Number mis-match");

                return true;
            }
            catch (Exception ex)
            {
                LogError("SeperateOutFieldDescriptors", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// Private SeperateOutRecords
        /// 
        /// Seperates out records.
        /// An ArrayList contains the reocrds, each stored as a Generic List of string data, 1 string per field.
        /// This method also calulates and stores the maximum length of each field seperately.
        /// </summary>
        /// <param name="validRecordsOnly">bool - Only Process Valid Records (Not marked as Deleted)</param>
        /// <returns>True or false depending on success</returns>
        private bool SeperateOutRecords(bool validRecordsOnly)
        {
            try
            {               
                recordsArray = new System.Collections.ArrayList();

                // First split out dataBuffer into indiviudal records (byte arrays) held within a ArrayList
                System.Collections.Generic.List<byte[]> tempArrayList = tempSeperateOutRecordsFromFile();

                System.Collections.Generic.List<string> recordDataList = new System.Collections.Generic.List<string>();

                maxFieldSize = new System.Collections.Generic.SortedList<int, int>();

                //Cycle through each record and split fields up.
                foreach (byte[] record in tempArrayList)
                {
                    int i = 0;
                    int recPos = 1; // Need to skip over first element in the record (Delete Flag)

                    int recordMarker = 0;           // Indicates the status of the record.

                    if (record[0] == recordValid)
                    {
                        // Record marked as valid
                        recordMarker = 0;
                    }
                    else if (record[0] == recordDeleted)
                    {
                        // Record marked as deleted
                        recordMarker = 1;
                    }

                    else
                    {
                        // Record Marker unknown.
                        recordMarker = -1;
                    }

                    if (checkValid(validRecordsOnly, recordMarker))
                    {
                        foreach(System.Collections.Generic.KeyValuePair<int, FieldInfo> kvp in fieldSortedList)
                        {
                            string temp = null;
                            int pos = 0;
                            int fieldSize = 0;

                            FieldInfo fieldInfo = (FieldInfo)kvp.Value;

                            while (pos < fieldInfo.fieldLength)
                            {
                                temp = temp + (char)(byte)record[recPos];
                                recPos++;
                                pos++;                                
                            }

                            recordDataList.Add(temp.Trim());

                            fieldSize = temp.Trim().Length;                            

                            if (maxFieldSize.Count >= (fieldInfo.fieldNumber))
                            {
                                int tempInt = maxFieldSize[fieldInfo.fieldNumber];
                                if (maxFieldSize[fieldInfo.fieldNumber] < fieldSize)
                                    maxFieldSize[fieldInfo.fieldNumber] = fieldSize;
                            }
                            else
                                maxFieldSize.Add(fieldInfo.fieldNumber,fieldSize);

                            i++;
                        }

                        if (i != numberOfFields)
                            LogError("SeperateOutRecords", "Error - Number of fields mis-match");

                        recordsArray.Add(recordDataList);
                    }

                    recordDataList = new System.Collections.Generic.List<string>();
                }

                numberOfValidRecords = recordsArray.Count;

                if (recordsArray.Count != numberOfRecords)
                    LogError("SeperateOutRecords", "Info - Number of records mis-match (possible due to invalid record flag");

                return true;
            }
            catch (Exception ex)
            {
                LogError("SeperateOutRecords", ex.Message);
                LogError("SeperateOutRecords", recordsArray.Count.ToString());

                return false;
            }
        }

        /// <summary>
        /// Private checkValid
        /// </summary>
        /// <param name="checkValid">bool - flag to run check or not</param>
        /// <param name="recordMarker">int - flag for valid recordMarker</param>
        /// <returns>true or false depending on success</returns>
        private bool checkValid(bool checkValid, int recordMarker)
        {
            if (!checkValid)
                return true;
            else
            {
                if (recordMarker == 0)
                    return true;
                else
                    return false;
            }
        }

        /// <summary>
        /// Private tempSeperateOutRecords
        /// 
        /// This method is called from the SeperateOutRecords method. Its been split out to make the code easier to manage.
        /// </summary>
        /// <returns>System.Collections.ArrayList</returns>
        private System.Collections.Generic.List<byte[]> tempSeperateOutRecordsFromFile()
        {
            System.Collections.IEnumerator iEnum = fileBuffer.GetEnumerator();
            int pos = 0;
            int dataStart = 0;

            Byte[] tempArray = new byte[lengthOfRecord];
            System.Collections.Generic.List<byte[]> tempArrayList = new System.Collections.Generic.List<byte[]>();

            while (iEnum.MoveNext())
            {

                if (dataStart >= dataOffset)
                {
                    if (pos < lengthOfRecord - 1)
                    {
                        tempArray[pos] = (byte)iEnum.Current;
                        pos++;
                    }
                    else
                    {
                        tempArrayList.Add(tempArray);

                        pos = 0;
                        tempArray = new byte[lengthOfRecord];
                    }
                }
                else
                    dataStart++;
            }

            return tempArrayList;
        }

        /// <summary>
        /// Private GetRecord
        /// 
        /// Gets the record stored within the recordsArray. The record is stored as a Generic List of strings.
        /// </summary>
        /// <param name="recordNo">int - Record number</param>
        /// <returns>Generic List of strings containing the record data</returns>
        private System.Collections.Generic.List<string> GetRecord(int recordNo)
        {
            try
            {
                return (System.Collections.Generic.List<string>)recordsArray[recordNo];
            }
            catch (Exception ex)
            {
                LogError("GetRecord", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// Private GetField
        /// 
        /// Gets the field within the record ArrayList, returns as a string.
        /// </summary>
        /// <param name="recordNo">Record number</param>
        /// <param name="fieldNo">Field number</param>
        /// <returns>string containing field data</returns>
        private string GetField(int recordNo, int fieldNo)
        {
            try
            {
                System.Collections.Generic.List<string> templist = GetRecord(recordNo);
                return (string)templist[fieldNo];
            }
            catch (Exception ex)
            {
                LogError("GetField", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// Private GetCommaDelimitedRecord
        /// 
        /// Converts a record into a Comma Delimited string
        /// </summary>
        /// <param name="recordNo">Record number</param>
        /// <returns>Comma delimited string containing record</returns>
        private string GetCommaDelimitedRecord(int recordNo)
        {
            try
            {
                System.Collections.Generic.List<string> tempRecordList = GetRecord(recordNo);

                string temp = "";

                for (int i = 0; i < numberOfFields; i++)
                {
                    if (i != numberOfFields - 1)
                        temp = temp + "\"" +(string)tempRecordList[i] +"\"" + ",";
                    else
                        temp = temp + "\"" +(string)tempRecordList[i] +"\"";
                }

                return temp;
            }
            catch (Exception ex)
            {
                LogError("GetCommaDelimitedRecord", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// Private GetCommaDelimitedFieldInfo
        /// 
        /// Creates a comma delimited string containing the field info:
        /// Field No, Field Name, Type, Width.
        /// </summary>
        /// <param name="fieldNo">Field number</param>
        /// <returns>Comma delimited string containing field info</returns>
        private string GetCommaDelimitedFieldInfo(int fieldNo)
        {
            try
            {
                FieldInfo fieldInfo = (FieldInfo)fieldSortedList[fieldNo];

                return fieldNo + "," + fieldInfo.fieldName + "," + fieldInfo.fieldType() + "," + fieldInfo.fieldLength.ToString();
            }
            catch (Exception ex)
            {
                LogError("GetCommaDelimitedFieldInfo", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// Private GetCommaDelimitedFieldNames
        /// 
        /// Creates a comma delimited string containing the field names.
        /// </summary>
        /// <returns>Comma delimited string containing the field names</returns>
        private string GetCommaDelimitedFieldNames()
        {
            try
            {
                string temp = "";

                for (int i = 1; i <= numberOfFields; i++)
                {
                    FieldInfo fieldInfo = (FieldInfo)fieldSortedList[i];

                    if (i != numberOfFields - 1)
                        temp = temp + fieldInfo.fieldName + ",";
                    else
                        temp = temp + fieldInfo.fieldName;
                }

                return temp;
            }
            catch (Exception ex)
            {
                LogError("GetCommaDelimitedFieldNames", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// Private SplitFileNameAndPath
        /// 
        /// Takes the full path and file name and splits it into two seperate strings, using index of last '\' to seperate them.
        /// </summary>
        /// <param name="fileNameToSplit">string of the path and file name</param>
        /// <returns>true or false depending on success</returns>
        private bool SplitFileNameAndPath(string fileNameToSplit)
        {
            try
            {
                int pos = fileNameToSplit.LastIndexOf('\\');

                path = fileNameToSplit.Substring(0, pos);
                fileName = fileNameToSplit.Substring(pos + 1);

                return true;
            }
            catch (Exception ex)
            {
                LogError("SplitFileNameAndPath", ex.Message);
                return false;
            }
        }

        /// <summary>
        /// Private AnalyseFields_DataCount
        /// 
        /// For a given field all records are checked to see if data is present and a count returned.
        /// </summary>
        /// <param name="fieldNo">Field number</param>
        /// <returns>int - count of records holding data for given field</returns>
        private int AnalyseFields_DataCount(int fieldNo)
        {
            try
            {
                int fieldRecordCount = 0;
                foreach (System.Collections.Generic.List<string> record in recordsArray)
                {
                    string fieldData = record[fieldNo];

                    if ((!String.IsNullOrEmpty(fieldData.Trim())) || (!String.IsNullOrWhiteSpace(fieldData.Trim())))
                        fieldRecordCount++;
                }

                return fieldRecordCount;
            }
            catch (Exception ex)
            {
                LogError("AnalyseFieldData", ex.Message);
                return -1;
            }
        }

        /// <summary>
        /// Private AnalyseRecords_FieldCount
        /// </summary>        
        /// <returns>System.Collections.SortedList - containing field data count</returns>
        private System.Collections.SortedList AnalyseRecords_FieldCount()
        {
            try
            {
                System.Collections.SortedList sortedList = new System.Collections.SortedList();

                for (int i = 0; i < numberOfFields; i++)
                {
                    sortedList.Add(i, AnalyseFields_DataCount(i));
                }

                return sortedList;
            }
            catch (Exception ex)
            {
                LogError("AnalyseRecordsData", ex.Message);
                return null;
            }
        }

        /// <summary>
        /// Private GetFieldName
        /// 
        /// Gets the Field Name for a given field number.
        /// </summary>
        /// <param name="fieldNo">Field Number</param>
        /// <returns>string - Field Name</returns>
        private string GetFieldName(int fieldNo)
        {
            try
            {
                FieldInfo fieldInfo = (FieldInfo)fieldSortedList[fieldNo];                

                return fieldInfo.fieldName;
            }
            catch (Exception ex)
            {
                LogError("GetFieldName", ex.Message);
                return null;
            }
        }

        // Private Methods to Clear Buffers
        private void ClearFileBuffer()
        {
            fileBuffer = null;
        }

        private void ClearFieldDescriptorBuffer()
        {
            fieldDescriptorBuffer = null;
        }

        /// <summary>
        /// Private StringToDateTime
        /// 
        /// Parse a string containing a DateTime, usign the format specified by DateTimeType (Dbase DateTime formats)
        /// </summary>
        /// <param name="stringToParse">string - dateTiime string to Parse</param>
        /// <param name="dateTimeType">DateTimeType - DateTime format of the string</param>
        /// <returns>DateTime object</returns>
        private DateTime StringToDateTime(string stringToParse, DateTimeType dateTimeType)
        {
            DateTime dateTime = new DateTime(0);

            switch (dateTimeType)
            {
                case DateTimeType.DbaseDate:
                    {
                        // Format is YYYYMMDD
                        if (stringToParse.Length == 8)
                        {
                            int temp = 0;
                            if (Int32.TryParse(stringToParse.Substring(0, 4), out temp))
                                dateTime = dateTime.AddYears(temp - 1);
                            else
                                LogError("StringToDateTime", "Unable to parse (Year) to DateTime the (DbaseDate) string: " + stringToParse);
                            if (Int32.TryParse(stringToParse.Substring(4, 2), out temp))
                                dateTime = dateTime.AddMonths(temp - 1);
                            else
                                LogError("StringToDateTime", "Unable to parse (Month) to DateTime the (DbaseDate) string: " + stringToParse);
                            if (Int32.TryParse(stringToParse.Substring(6, 2), out temp))
                                dateTime = dateTime.AddDays(temp - 1);
                            else
                                LogError("StringToDateTime", "Unable to parse (Day) to DateTime the (DbaseDate) string: " + stringToParse);
                        }
                        else
                        {
                            LogError("StringToDateTime", "Unable to parse to DateTime the (DbaseDate) string: " + stringToParse);
                        }
                        break;
                    }
                case DateTimeType.DbaseDateTime:
                    LogError("StringToDateTime", "Unable to parse to DateTime the (DbaseDateTime) string: " + stringToParse);
                    break;
                case DateTimeType.DBaseTimeStamp:
                    {
                        long tempDate = 0;
                        long tempTime = 0;

                        if (long.TryParse(stringToParse.Substring(0, 4), out tempDate))
                            if (long.TryParse(stringToParse.Substring(4, 4), out tempTime))
                            {
                                LogError("StringToDateTime", "To Be Implemented: " + stringToParse);
                            }
                            else
                                LogError("StringToDateTime", "Unable to parse (Time)to DateTime the (DBaseTimeStamp) string: " + stringToParse);
                        else
                            LogError("StringToDateTime", "Unable to parse (Date)to DateTime the (DBaseTimeStamp) string: " + stringToParse);

                        LogError("StringToDateTime", "Unable to parse to DateTime the (DBaseTimeStamp) string: " + stringToParse);
                    }
                    break;
            }

            return dateTime;
        }

// ************************************************************************************************
// Helper methods - more generic
// ************************************************************************************************

        /// <summary>
        /// readInt16
        /// Read 16-bit integer from mBuffer[offset]
        /// </summary>
        /// <param name="offset">int - the offset in the mBuffer</param>
        /// <param name="mBuffer">byte[] - byte array</param>
        /// <returns>int</returns>
        private int ReadInt16(int offset, byte[] mBuffer)
        {
            try
            {
                return BitConverter.ToUInt16(mBuffer, offset);
            }
            catch (Exception ex)
            {
                LogError("ReadInt16", ex.Message);
                return -1;
            }
        }

        //*************************************************************************************************
        // The following set of methods convert a String into different DataTypes

        private int StringToInt(string stringToParse)
        {
            int temp = 0;
            if (Int32.TryParse(stringToParse, out temp))
                return temp;
            else
            {
                LogError("StringToInt", "Unable to parse to int the string: " + stringToParse);
                return 0;
            }
        }

        private long StringToLong(string stringToParse)
        {
            long temp = 0;
            if (long.TryParse(stringToParse, out temp))
                return temp;
            else
            {
                LogError("StringToLong", "Unable to parse to long the string: " + stringToParse);
                return 0;
            }
        }

        private double StringToDouble(string stringToParse)
        {
            double temp = 0.0;
            if (double.TryParse(stringToParse, out temp))
                return temp;
            else
            {
                LogError("StringToDouble", "Unable to parse to double the string: " + stringToParse);
                return 0;
            }
        }

        private float StringToFloat(string stringToParse)
        {
            float temp = 0;
            if (float.TryParse(stringToParse, out temp))
                return temp;
            else
            {
                LogError("StringToFloat", "Unable to parse to float the string: " + stringToParse);
                return 0;
            }
        }

        private bool StringToBool(string stringToParse)
        {
            bool temp = true;
            switch (stringToParse.ToLower())    // Convert string to lower case before looking at data, just to make life easier.
            {
                case "y":
                case "t":
                    temp = true;
                    break;
                case "n":
                case "f":
                    temp = false;
                    break;                

                default:
                    LogError("StringToBool", "Unable to parse to bool the string" + stringToParse);
                    break;
            }

            return temp;
        }
        
// ************************************************************************************************
// Exception Logging methods
// ************************************************************************************************

        /// <summary>
        /// CreateErrorLogFile
        /// 
        /// Creates Error log file in the same directory as the input file
        /// </summary>
        private bool CreateErrorLogFile()
        {
            try
            {
                swErrorLog = File.CreateText(path + "\\DbasereaderError.log");
                return true;
            }
            catch (IOException ioEx)
            {
                string temp = ioEx.Message;
                return false;
            }
            catch (UnauthorizedAccessException uaEx)
            {
                string temp = uaEx.Message;
                return false;
            }
            catch (Exception ex)
            {
                string temp = ex.Message;
                return false;
            }
        }

        /// <summary>
        /// LogError
        /// 
        /// Writes a new line to Error Log
        /// </summary>
        /// <param name="method">string for the method name to aid debugging</param>
        /// <param name="exceptionMessage">string for the excpetion message (String indead of Execption type so any message can be entered)</param>
        private void LogError(string method, string exceptionMessage)
        {
            try
            {

                DateTime dateTime = DateTime.Now;
                string logEntry = dateTime.ToShortDateString() + " , " + dateTime.ToShortTimeString() + " , " + method + " , " + exceptionMessage;

                swErrorLog.WriteLine(logEntry);

                swErrorLog.Flush();
            }
            catch (IOException ioEx)
            {
                string temp = ioEx.Message;
            }
            catch (Exception ex)
            {
                string temp = ex.Message;
            }
        }

        /// <summary>
        /// CloseLog
        /// 
        /// Closes open log file.
        /// </summary>
        private void CloseLog()
        {
            swErrorLog.Close();
        }
    }
}